﻿// <copyright file="SearchService.cs" company="Clever Software">
// Copyright (c) 2013, 2014 All Right Reserved, http://www.cleversoftware.by
//
// This source is subject to the Microsoft Permissive License.
// Please see the License.txt file for more information.
// All other rights reserved.
//
// THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY 
// KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
// PARTICULAR PURPOSE.
//
// </copyright>
// <author>Clever Software</author>
// <email>info@cleversoftware.by</email>
// <date>2014-02-04</date> 
// <summary>CleverCMS source code</summary>

namespace CleverCMS.Core.Services
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
   
    using Lucene.Net.Analysis.Ru;
    using Lucene.Net.Documents;
    using Lucene.Net.Index;
    using Lucene.Net.QueryParsers;
    using Lucene.Net.Search;
    using Lucene.Net.Store;

    using Version = Lucene.Net.Util.Version;
    using Directory = System.IO.Directory;

    using Blog.Data.Entities;
    using Cms.Data.Entities;
    using Core.Services.ViewModels;

    public class SearchService
    {
        public static string LuceneDir = Path.Combine(System.Web.Hosting.HostingEnvironment.MapPath("~/"), "LuceneIndex\\Search");
       
        private static FSDirectory m_directoryTemp;
        private static FSDirectory m_directory
        {
            get
            {
                if (m_directoryTemp == null)
                {
                    if (!Directory.Exists(LuceneDir)) 
                        Directory.CreateDirectory(LuceneDir);
                    
                    m_directoryTemp = FSDirectory.Open(new DirectoryInfo(LuceneDir));
                }

                if (IndexWriter.IsLocked(m_directoryTemp)) 
                    IndexWriter.Unlock(m_directoryTemp);

                var lockFilePath = Path.Combine(LuceneDir, "write.lock");
                
                if (File.Exists(lockFilePath)) 
                    File.Delete(lockFilePath);
                
                return m_directoryTemp;
            }
        }

        /// <summary>
        /// Returns all records
        /// </summary>
        /// <returns></returns>
        public static IEnumerable<SearchModel> GetAllIndexRecords()
        {
            if (!Directory.EnumerateFiles(LuceneDir).Any())
                return new List<SearchModel>();

            var searcher = new IndexSearcher(m_directory, false);
            var reader = IndexReader.Open(m_directory, false);
            var docs = new List<Document>();
            var term = reader.TermDocs();
            
            while (term.Next()) 
                docs.Add(searcher.Doc(term.Doc));
            
            reader.Dispose();
            searcher.Dispose();

            return _mapLuceneToDataList(docs);
        }
        /// <summary>
        /// Search functions
        /// </summary>
        /// <param name="input"></param>
        /// <param name="fieldName"></param>
        /// <returns></returns>
        public static IEnumerable<SearchModel> Search(string input, string fieldName = "")
        {
            if (string.IsNullOrEmpty(input)) return new List<SearchModel>();

            IEnumerable<string> terms = input.Trim().Replace("-", " ").Split(' ').Where(x => !string.IsNullOrEmpty(x)).Select(x => x.Trim() + "*");
            input = string.Join(" ", terms);

            return _search(input, fieldName);
        }
        /// <summary>
        /// Search functions
        /// </summary>
        /// <param name="input"></param>
        /// <param name="fieldName"></param>
        /// <returns></returns>
        public static IEnumerable<SearchModel> SearchDefault(string input, string fieldName = "")
        {
            return string.IsNullOrEmpty(input) ? new List<SearchModel>() : _search(input, fieldName);
        }
        /// <summary>
        ///  Convert input Page or Entry in output List
        /// </summary>
        /// <param name="obj"></param>
        public static void AddUpdateLuceneIndex(Object obj)
        {
            AddUpdateLuceneIndex(new List<Object> { obj });
        }
        /// <summary>
        /// Create or Update
        /// </summary>
        /// <param name="objects"></param>
        /// <returns></returns>
        public static int AddUpdateLuceneIndex(IEnumerable<Object> objects)
        {
            int count = 0;
            RussianAnalyzer analyzer = null;
            IndexWriter writer = null;
            try
            {
                using (analyzer = new RussianAnalyzer(Version.LUCENE_30))
                {
                    using (writer = new IndexWriter(m_directory, analyzer, IndexWriter.MaxFieldLength.UNLIMITED))
                    {
                        foreach (var obj in objects)
                        {
                            _addToLuceneIndex(obj, writer);
                            count++;
                        }
                        analyzer.Close();
                        writer.Dispose();  
                    }
                }
            }
            catch (Exception ex)
            {
                count = 0;
                throw ex;
            }
            finally
            {
                if (analyzer != null) analyzer.Close();
                if (writer != null) writer.Dispose();
            }
            return count;
        }
        /// <summary>
        /// Delete from index
        /// </summary>
        /// <param name="recordAlias"></param>
        public static void ClearLuceneIndexRecord(string recordAlias) 
        {
            var analyzer = new RussianAnalyzer(Version.LUCENE_30);
            using (var writer = new IndexWriter(m_directory, analyzer, IndexWriter.MaxFieldLength.UNLIMITED))
            {
                var searchQuery = new TermQuery(new Term("Alias", recordAlias));
                writer.DeleteDocuments(searchQuery);

                analyzer.Close();
                writer.Dispose();
            }
        }
        /// <summary>
        /// Delete All
        /// </summary>
        /// <returns></returns>
        public static bool ClearLuceneIndex()
        {
            try
            {
                var analyzer = new RussianAnalyzer(Version.LUCENE_30);
                using (var writer = new IndexWriter(m_directory, analyzer, true, IndexWriter.MaxFieldLength.UNLIMITED))
                {
                    writer.DeleteAll();
                    analyzer.Close();
                    writer.Dispose();
                }
            }
            catch (Exception)
            {
                return false;
            }
            return true;
        }
        /// <summary>
        /// Сompression
        /// </summary>
        public static void Optimize()
        {
            var analyzer = new RussianAnalyzer(Version.LUCENE_30);
            using (var writer = new IndexWriter(m_directory, analyzer, IndexWriter.MaxFieldLength.UNLIMITED))
            {
                analyzer.Close();
                writer.Optimize();
                writer.Dispose();
            }
        }

        private static SearchModel _mapLuceneDocumentToData(Document doc)
        {
            return new SearchModel
            {
                Title = doc.Get("Title"),
                Alias = doc.Get("Alias"),
                Content = doc.Get("Content"),
                Category = doc.Get("Category"),
                CreatedAt = DateTools.StringToDate(doc.Get("CreatedAt")),
                IsPage = Convert.ToBoolean(doc.Get("IsPage"))
            };
        }

        private static void _addToLuceneIndex(Object obj, IndexWriter writer)
        {
            var doc = new Document();

            // if object is Page
            var objPage = obj as CmsPage;
            if (objPage != null)
            {
                var searchQuery = new TermQuery(new Term("Alias", objPage.Alias));
                writer.DeleteDocuments(searchQuery);

                doc.Add(new Field("Title", objPage.PageTitle, Field.Store.YES, Field.Index.ANALYZED));
                doc.Add(new Field("Alias", objPage.Alias ?? "", Field.Store.YES, Field.Index.NOT_ANALYZED));
                doc.Add(new Field("Content", objPage.PageContent ?? "", Field.Store.YES, Field.Index.ANALYZED));
                doc.Add(new Field("Category", "", Field.Store.YES, Field.Index.ANALYZED));
                doc.Add(new Field("CreatedAt", DateTools.DateToString(objPage.CreatedAt, DateTools.Resolution.DAY), Field.Store.YES, Field.Index.ANALYZED));
                doc.Add(new Field("IsPage", true.ToString(), Field.Store.YES, Field.Index.NOT_ANALYZED));
            }

            // if object is BlogEntry
            var objEntry = obj as BlogEntry;
            if (objEntry != null)
            {
                var searchQuery = new TermQuery(new Term("Alias", objEntry.Alias));
                writer.DeleteDocuments(searchQuery);

                doc.Add(new Field("Title", objEntry.Header, Field.Store.YES, Field.Index.ANALYZED));
                doc.Add(new Field("Alias", objEntry.Alias ?? "", Field.Store.YES, Field.Index.NOT_ANALYZED));
                doc.Add(new Field("Content", objEntry.Content ?? "", Field.Store.YES, Field.Index.ANALYZED));
                doc.Add(new Field("Category", objEntry.Category != null ? objEntry.Category.CategoryName : "", Field.Store.YES, Field.Index.ANALYZED));
                doc.Add(new Field("CreatedAt", DateTools.DateToString(objEntry.Created, DateTools.Resolution.DAY), Field.Store.YES, Field.Index.ANALYZED));
                doc.Add(new Field("IsPage", false.ToString(), Field.Store.YES, Field.Index.NOT_ANALYZED));
            }

            writer.AddDocument(doc);
        }

        private static IEnumerable<SearchModel> _search(string searchQuery, string searchField = "")
        {
            if (string.IsNullOrEmpty(searchQuery.Replace("*", "").Replace("?", ""))) return new List<SearchModel>();

            using (var searcher = new IndexSearcher(m_directory, false))
            {
                var hits_limit = 1000;
                var analyzer = new RussianAnalyzer(Version.LUCENE_30);

                if (!string.IsNullOrEmpty(searchField))
                {
                    var parser = new QueryParser(Version.LUCENE_30, searchField, analyzer);
                    parser.AllowLeadingWildcard = true;
                    var query = ParseQuery(searchQuery, parser);

                    var hits = searcher.Search(query, hits_limit).ScoreDocs;
                    var results = _mapLuceneToDataList(hits, searcher);

                    analyzer.Close();
                    searcher.Dispose();

                    return results;
                }
                else
                {
                    var boosts = new Dictionary<string, float>();

                    boosts.Add("Title", 1f);
                    boosts.Add("Alias", 1f);
                    boosts.Add("Content", 1f);

                    var parser = new MultiFieldQueryParser(Version.LUCENE_30, new[] { "Title", "Alias", "Content" }, analyzer, boosts);
                    parser.AllowLeadingWildcard = true;
                    var query = ParseQuery(searchQuery, parser);
                    var hits = searcher.Search(query, null, hits_limit, Sort.RELEVANCE).ScoreDocs;
                    var results = _mapLuceneToDataList(hits, searcher);

                    analyzer.Close();
                    searcher.Dispose();

                    return results;
                }
            }
        }

        private static Query ParseQuery(string searchQuery, QueryParser parser)
        {
            Query query;
            try
            {
                query = parser.Parse(searchQuery.Trim());
            }
            catch (ParseException)
            {
                query = parser.Parse(QueryParser.Escape(searchQuery.Trim()));
            }
            return query;
        }

        private static IEnumerable<SearchModel> _mapLuceneToDataList(IEnumerable<Document> hits)
        {
            return hits.Select(_mapLuceneDocumentToData).ToList();
        }

        private static IEnumerable<SearchModel> _mapLuceneToDataList(IEnumerable<ScoreDoc> hits, IndexSearcher searcher)
        {
            return hits.Select(hit => _mapLuceneDocumentToData(searcher.Doc(hit.Doc))).ToList();
        }

    }
}